package crmdna.inventory;

import static crmdna.common.AssertUtils.ensure;
import static crmdna.common.AssertUtils.ensureEqual;
import static crmdna.common.AssertUtils.ensureNotNull;
import static crmdna.common.OfyService.ofy;
import static crmdna.common.ProjectionQuery.pq;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Set;

import com.google.appengine.api.datastore.AsyncDatastoreService;
import com.google.appengine.api.datastore.DatastoreServiceFactory;
import com.google.appengine.api.datastore.Entity;
import com.google.appengine.api.datastore.FetchOptions;
import com.google.appengine.api.datastore.PropertyProjection;
import com.google.appengine.api.datastore.Query.FilterOperator;
import com.google.appengine.api.memcache.MemcacheServiceFactory;
import com.google.gson.Gson;
import com.googlecode.objectify.Key;
import com.googlecode.objectify.cmd.Query;
import com.googlecode.objectify.cmd.QueryKeys;

import crmdna.client.Client;
import crmdna.common.APIException;
import crmdna.common.APIResponse.Status;
import crmdna.common.DSUtils;
import crmdna.common.UnitUtils;
import crmdna.common.UnitUtils.PhysicalQuantity;
import crmdna.common.UnitUtils.ReportingUnit;
import crmdna.common.Utils;
import crmdna.group.Group;
import crmdna.group.Group.GroupProp;
import crmdna.sequence.Sequence;
import crmdna.sequence.Sequence.SequenceType;
import crmdna.user.User;
import crmdna.user.User.Action;
import crmdna.user.User.ResourceType;

public class InventoryItem {

	public static InventoryItemProp create(String client, long groupId,
			long inventoryItemTypeId, String displayName,
			PhysicalQuantity physicalQuantity, ReportingUnit reportingUnit,
			double initialQuantityInReportingUnit, String login) {

		Client.ensureValid(client);
		User.ensureAccess(client, login, ResourceType.INVENTORY_ITEM, groupId,
				Action.WRITE);

		GroupProp groupProp = Group.safeGet(client, groupId).toProp();

		ensureNotNull(displayName, "Display name cannot be null");
		ensure(displayName.length() > 0, "Display name cannot be empty");

		// first character should be an alphabet
		char c = displayName.toLowerCase().charAt(0);
		ensure(c >= 'a' && c <= 'z', "First character should be an alphabet");

		double quantityInDefaultUnit = UnitUtils
				.safeGetQuantityInDefaultUnit(physicalQuantity,
						initialQuantityInReportingUnit, reportingUnit);

		String name = Utils.removeSpacesAndHyphen(displayName.toLowerCase());
		List<Key<InventoryItemEntity>> keys = ofy(client).load()
				.type(InventoryItemEntity.class).filter("name", name)
				.filter("groupId", groupId).keys().list();

		if (keys.size() != 0)
			throw new APIException().status(
					Status.ERROR_RESOURCE_ALREADY_EXISTS).message(
					"There is already an inventory item with the same name for group ["
							+ groupProp.displayName + "]");

		String key = getUniqueKey(client, groupId, name);
		long val = MemcacheServiceFactory.getMemcacheService().increment(key,
				1, (long) 0);

		if (val != 1)
			throw new APIException(Status.ERROR_RESOURCE_ALREADY_EXISTS,
					"There is already a inventory item with the same name for group ["
							+ groupProp.displayName + "]");

		InventoryItemType.safeGet(client, inventoryItemTypeId);

		InventoryItemEntity entity = new InventoryItemEntity();
		entity.inventoryItemId = Sequence.getNext(client,
				SequenceType.INVENTORY_ITEM);
		entity.groupId = groupId;
		entity.displayName = displayName;
		entity.physicalQuantity = physicalQuantity;
		entity.inventoryItemTypeId = inventoryItemTypeId;
		entity.groupId = groupId;
		entity.reportingUnit = reportingUnit;
		entity.quantityInDefaultUnit = quantityInDefaultUnit;

		// populate dependents
		entity.name = name;
		entity.firstChar = entity.name.substring(0, 1);

		ofy(client).save().entity(entity).now();

		return entity.toProp();
	}

	public static InventoryItemProp update(String client, long inventoryItemId,
			Long newInventoryItemTypeId, String newDisplayName,
			ReportingUnit newReportingUnit, String login) {

		Client.ensureValid(client);
		InventoryItemEntity entity = safeGet(client, inventoryItemId);
		User.ensureAccess(client, login, ResourceType.INVENTORY_ITEM,
				entity.groupId, Action.WRITE);

		GroupProp groupProp = Group.safeGet(client, entity.groupId).toProp();

		String name = null;
		if (newDisplayName != null) {
			ensure(newDisplayName.length() > 0, "Display name cannot be empty");
			char c = newDisplayName.toLowerCase().charAt(0);
			ensure(c >= 'a' && c <= 'z',
					"First character of display name should be an alphabet");

			name = Utils.removeSpacesAndHyphen(newDisplayName.toLowerCase());

			if (!entity.name.equals(name)) {
				// ensure name doesn't clash with another existing entity in the
				// same group
				List<Key<InventoryItemEntity>> keys = ofy(client).load()
						.type(InventoryItemEntity.class).filter("name", name)
						.filter("groupId", entity.groupId).keys().list();

				if (keys.size() != 0)
					throw new APIException().status(
							Status.ERROR_RESOURCE_ALREADY_EXISTS).message(
							"There is already an inventory item with the same name for group ["
									+ groupProp.displayName + "]");

				String key = getUniqueKey(client, entity.groupId, name);
				long val = MemcacheServiceFactory.getMemcacheService()
						.increment(key, 1, (long) 0);

				if (val != 1) {
					throw new APIException(
							Status.ERROR_RESOURCE_ALREADY_EXISTS,
							"There is already a inventory item with the same name for group ["
									+ groupProp.displayName + "]");
				}
			}
		}

		if (newInventoryItemTypeId != null) {
			InventoryItemType.safeGet(client, newInventoryItemTypeId);
		}

		if (newReportingUnit != null) {
			UnitUtils.safeGetQuantityInDefaultUnit(entity.physicalQuantity, 0,
					newReportingUnit);
		}

		// all ok. populate and save
		if (newDisplayName != null) {
			entity.displayName = newDisplayName;

			entity.name = name;
			entity.firstChar = entity.name.substring(0, 1);
		}

		if (newInventoryItemTypeId != null)
			entity.inventoryItemTypeId = newInventoryItemTypeId;

		if (newReportingUnit != null)
			entity.reportingUnit = newReportingUnit;

		ofy(client).save().entity(entity).now();
		return entity.toProp();
	}

	private static String getUniqueKey(String namespace, long groupId,
			String name) {

		return namespace + "_" + SequenceType.INVENTORY_ITEM + "_" + groupId
				+ "_" + name;
	}

	public static InventoryItemEntity safeGet(String client,
			long inventoryItemId) {

		Client.ensureValid(client);

		InventoryItemEntity entity = ofy(client).load()
				.type(InventoryItemEntity.class).id(inventoryItemId).get();
		if (null == entity)
			throw new APIException(Status.ERROR_RESOURCE_NOT_FOUND,
					"Inventory item id  [" + inventoryItemId
							+ "] does not exist");

		return entity;
	}
	
	public static InventoryItemEntity safeGetByName(String client,
			String name) {

		Client.ensureValid(client);
		ensureNotNull(name);

		name = Utils.removeSpacesAndHyphen(name.toLowerCase());
		List<InventoryItemEntity> entities = ofy(client).load()
				.type(InventoryItemEntity.class).filter("name", name)
				.list();

		if (entities.size() == 0)
			throw new APIException(Status.ERROR_RESOURCE_NOT_FOUND, "Inventory item ["
					+ name + "] does not exist");

		if (entities.size() > 1)
			throw new APIException().status(Status.ERROR_RESOURCE_INCORRECT)
					.message(
							"Found [" + entities.size()
									+ "] matches for inventory item [" + name
									+ "]. Please specify Id");
		return entities.get(0);
	}

	public static Map<Long, InventoryItemEntity> get(String client,
			Set<Long> inventoryItemIds) {

		Client.ensureValid(client);

		Map<Long, InventoryItemEntity> map = ofy(client).load()
				.type(InventoryItemEntity.class).ids(inventoryItemIds);

		return map;
	}

	public static List<InventoryItemProp> query_to_be_removed(String client,
			InventoryItemQueryCondition qc) {

		List<Key<InventoryItemEntity>> keys = queryKeys(client, qc).list();

		List<Long> ids = new ArrayList<>();
		List<InventoryItemProp> props = new ArrayList<>(keys.size());

		for (Key<InventoryItemEntity> key : keys) {
			long id = key.getId();
			ids.add(id);

			InventoryItemProp prop = new InventoryItemProp();
			prop.inventoryItemId = id;
			props.add(prop);
		}

		List<com.google.appengine.api.datastore.Key> rawKeys = new ArrayList<>();
		for (Key key : keys) {
			rawKeys.add(key.getRaw());
		}

		System.out.println("ids = " + new Gson().toJson(ids));

		ensureEqual(keys.size(), props.size());

		AsyncDatastoreService datastore = DatastoreServiceFactory
				.getAsyncDatastoreService();

		// com.google.appengine.api.datastore.Query.Filter filter = new
		// com.google.appengine.api.datastore.Query.FilterPredicate(
		// "inventoryItemId", FilterOperator.IN, ids);

		com.google.appengine.api.datastore.Query.Filter filter = new com.google.appengine.api.datastore.Query.FilterPredicate(
				Entity.KEY_RESERVED_PROPERTY, FilterOperator.IN, rawKeys);

		com.google.appengine.api.datastore.Query q = new com.google.appengine.api.datastore.Query(
				InventoryItemEntity.class.getSimpleName());

		q.setFilter(filter).addProjection(
				new PropertyProjection("groupId", Long.class));

		Iterable<Entity> groupIds = datastore.prepare(q).asList(
				FetchOptions.Builder.withLimit(10000));

		q = new com.google.appengine.api.datastore.Query("InventoryItemEntity");
		q.setFilter(filter).addProjection(
				new PropertyProjection("inventoryItemTypeId", Long.class));

		Iterable<Entity> inventoryItemTypeIds = datastore.prepare(q)
				.asIterable();

		int i = 0;
		for (Entity entity : groupIds) {

			InventoryItemProp prop = props.get(i);
			prop.groupId = (long) entity.getProperty("groupId");
			i++;
		}
		System.out.println("i = " + i + ", key.size() = " + keys.size());

		i = 0;
		for (Entity entity : inventoryItemTypeIds) {

			InventoryItemProp prop = props.get(i);
			prop.inventoryItemTypeId = (long) entity
					.getProperty("inventoryItemTypeId");
			i++;
		}
		ensureEqual(keys.size(), i);

		System.out.println("before populateDependents. props: "
				+ new Gson().toJson(props));

		InventoryItemProp.populateDependents(client, props);

		Collections.sort(props);

		return props;
	}

	public static List<InventoryItemProp> query(String client,
			InventoryItemQueryCondition qc, String login) {

		User.ensureValidUser(client, login);

		List<Key<InventoryItemEntity>> keys = queryKeys(client, qc).list();

		ensureNotNull(keys);

		Collection<InventoryItemEntity> entities = ofy(client).load()
				.keys(keys).values();

		List<InventoryItemProp> props = new ArrayList<>(keys.size());
		for (InventoryItemEntity entity : entities) {
			props.add(entity.toProp());
		}

		InventoryItemProp.populateDependents(client, props);

		Collections.sort(props);
		return props;
	}

	public static List<InventoryItemProp> query2_to_be_removed(String client,
			InventoryItemQueryCondition qc) {

		List<Key<InventoryItemEntity>> keys = queryKeys(client, qc).list();

		List<Long> ids = new ArrayList<>();
		List<InventoryItemProp> props = new ArrayList<>(keys.size());

		for (Key<InventoryItemEntity> key : keys) {
			long id = key.getId();
			ids.add(id);

			InventoryItemProp prop = new InventoryItemProp();
			prop.inventoryItemId = id;
			props.add(prop);
		}

		String kind = "InventoryItemEntity";
		// List<Long> groupIds = DSUtils.executeProjectionQuery(kind, keys,
		// "groupId", Long.class);

		List<Long> groupIds = DSUtils.executeProjectionQuery2(
				InventoryItemEntity.class, keys, "groupId", Long.class);

		List<Long> inventoryItemTypeIds = DSUtils.executeProjectionQuery(kind,
				keys, "inventoryItemTypeId", Long.class);

		ensureEqual(keys.size(), groupIds.size(),
				"Records returned by projection query [" + groupIds.size()
						+ "] is different from number of keys [" + keys.size()
						+ "]");
		ensureEqual(keys.size(), inventoryItemTypeIds.size(),
				"Projection query result mismatch");

		for (int i = 0; i < keys.size(); i++) {
			InventoryItemProp prop = props.get(i);
			prop.groupId = groupIds.get(i);
			prop.inventoryItemTypeId = inventoryItemTypeIds.get(i);
		}

		InventoryItemProp.populateDependents(client, props);

		Collections.sort(props);

		return props;
	}

	public static List<InventoryItemProp> query3_to_be_removed(String client,
			InventoryItemQueryCondition qc) {

		List<Key<InventoryItemEntity>> keys = queryKeys(client, qc).list();

		List<Long> ids = new ArrayList<>();
		List<InventoryItemProp> props = new ArrayList<>(keys.size());

		for (Key<InventoryItemEntity> key : keys) {
			long id = key.getId();
			ids.add(id);

			InventoryItemProp prop = new InventoryItemProp();
			prop.inventoryItemId = id;
			props.add(prop);
		}

		List<Long> groupIds = pq(InventoryItemEntity.class, Long.class)
				.keys(keys).property("groupId").execute();

		List<Long> inventoryItemTypeIds = pq(InventoryItemEntity.class,
				Long.class).keys(keys).property("inventoryItemTypeId")
				.execute();

		ensureEqual(keys.size(), groupIds.size(),
				"Records returned by projection query [" + groupIds.size()
						+ "] is different from number of keys [" + keys.size()
						+ "]");
		ensureEqual(keys.size(), inventoryItemTypeIds.size(),
				"Projection query result mismatch");

		for (int i = 0; i < keys.size(); i++) {
			InventoryItemProp prop = props.get(i);
			prop.groupId = groupIds.get(i);
			prop.inventoryItemTypeId = inventoryItemTypeIds.get(i);
		}

		InventoryItemProp.populateDependents(client, props);

		Collections.sort(props);

		return props;
	}

	static QueryKeys<InventoryItemEntity> queryKeys(String client,
			InventoryItemQueryCondition qc) {

		Client.ensureValid(client);

		ensureNotNull(qc);

		Query<InventoryItemEntity> query = ofy(client).load().type(
				InventoryItemEntity.class);

		if (qc.groupId != null) {
			query = query.filter("groupId", qc.groupId);
		}

		if ((qc.inventoryItemTypeIds != null)
				&& !qc.inventoryItemTypeIds.isEmpty()) {
			query = query.filter("inventoryItemTypeId in",
					qc.inventoryItemTypeIds);
		}

		if ((qc.firstChars != null) && !qc.firstChars.isEmpty()) {
			query = query.filter("firstChar in", qc.firstChars);
		}

		return query.keys();
	}

	public static InventoryItemProp updateQuantity(final String client,
			final long inventoryItemId, final double changeInReportingUnit,
			final ReportingUnit reportingUnit, String changeDescription,
			Long departmentId, final boolean allowNegativeQuantity,
			final String login) {

		InventoryItemEntity entity = safeGet(client, inventoryItemId);

		User.ensureAccess(client, login, ResourceType.INVENTORY_ITEM,
				entity.groupId, Action.UPDATE_QUANTITY);

		double changeInDefaultUnit = UnitUtils.safeGetQuantityInDefaultUnit(
				entity.physicalQuantity, changeInReportingUnit, reportingUnit);

		if (!allowNegativeQuantity) {
			if (entity.quantityInDefaultUnit + changeInDefaultUnit < 0)
				throw new APIException()
						.status(Status.ERROR_RESOURCE_INCORRECT).message(
								"Negative quantity not allowed");
		}

		entity.quantityInDefaultUnit += changeInDefaultUnit;

		// There is a small chance of race condition. should be inside
		// transaction
		ofy(client).save().entity(entity);

		// record the change
		InventoryItemChangeProp prop = new InventoryItemChangeProp();
		prop.changeDescription = changeDescription;
		prop.changeInDefaultUnit = changeInDefaultUnit;
		prop.departmentId = departmentId;
		prop.inventoryItemId = inventoryItemId;
		prop.login = login;
		prop.ms = new Date().getTime();

		InventoryItemChange.recordChange(client, entity.groupId, prop);

		return entity.toProp();
	}

	public static void delete(String client, long inventoryItemId, String login) {
		Client.ensureValid(client);

		InventoryItemEntity entity = safeGet(client, inventoryItemId);

		User.ensureAccess(client, login, ResourceType.INVENTORY_ITEM,
				entity.groupId, Action.WRITE);

		ofy(client).delete().entity(entity);
	}
}
